package com.explorex.infra.puma.gateway.netty.rrmp.codec;


import com.explorex.infra.puma.gateway.netty.common.enums.BusinessType;
import com.explorex.infra.puma.gateway.netty.rrmp.RrmpMessage;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.TooLongFrameException;
import io.netty.util.ReferenceCountUtil;
import lombok.extern.slf4j.Slf4j;

import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.List;

/**
 * @author: JacceYang
 * @createTime: 2022/09/03 17:31
 * @description: ProxyMessageHandler
 */
@Slf4j
public class ConnectorLengthFieldBasedFrameDecoder extends ByteToMessageDecoder {

    private final ByteOrder byteOrder;
    private final int maxFrameLength;
    private final int lengthFieldOffset;
    private final int lengthFieldLength;
    private final int lengthFieldEndOffset;
    private final int lengthAdjustment;
    private final int initialBytesToStrip;
    private final int magicLength = 1;
    private final int businessLength = 2;
    private final int bodyCheckLength = 3;
    private final int headerLength = 12;
    private final boolean failFast;
    private boolean discardingTooLongFrame;
    private long tooLongFrameLength;
    private long bytesToDiscard;

    /**
     * Creates a new instance.
     *
     * @param maxFrameLength      the maximum length of the frame.  If the length of the frame is
     *                            greater than this value, {@link TooLongFrameException} will be
     *                            thrown.
     * @param lengthFieldOffset   the offset of the length field
     * @param lengthFieldLength   the length of the length field
     * @param lengthAdjustment    the compensation value to add to the value of the length field
     * @param initialBytesToStrip the number of first bytes to strip out from the decoded frame
     */
    public ConnectorLengthFieldBasedFrameDecoder(
            int maxFrameLength,
            int lengthFieldOffset, int lengthFieldLength,
            int lengthAdjustment, int initialBytesToStrip) {
        this(
                maxFrameLength,
                lengthFieldOffset, lengthFieldLength, lengthAdjustment,
                initialBytesToStrip, true);
    }

    /**
     * Creates a new instance.
     *
     * @param maxFrameLength      the maximum length of the frame.  If the length of the frame is
     *                            greater than this value, {@link TooLongFrameException} will be
     *                            thrown.
     * @param lengthFieldOffset   the offset of the length field
     * @param lengthFieldLength   the length of the length field
     * @param lengthAdjustment    the compensation value to add to the value of the length field
     * @param initialBytesToStrip the number of first bytes to strip out from the decoded frame
     * @param failFast            If <tt>true</tt>, a {@link TooLongFrameException} is thrown as
     *                            soon as the decoder notices the length of the frame will exceed
     *                            <tt>maxFrameLength</tt> regardless of whether the entire frame
     *                            has been read.  If <tt>false</tt>, a {@link TooLongFrameException}
     *                            is thrown after the entire frame that exceeds <tt>maxFrameLength</tt>
     *                            has been read.
     */
    public ConnectorLengthFieldBasedFrameDecoder(
            int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
            int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
        this(
                ByteOrder.BIG_ENDIAN, maxFrameLength, lengthFieldOffset, lengthFieldLength,
                lengthAdjustment, initialBytesToStrip, failFast);
    }

    /**
     * Creates a new instance.
     *
     * @param byteOrder           the {@link ByteOrder} of the length field
     * @param maxFrameLength      the maximum length of the frame.  If the length of the frame is
     *                            greater than this value, {@link TooLongFrameException} will be
     *                            thrown.
     * @param lengthFieldOffset   the offset of the length field
     * @param lengthFieldLength   the length of the length field
     * @param lengthAdjustment    the compensation value to add to the value of the length field
     * @param initialBytesToStrip the number of first bytes to strip out from the decoded frame
     * @param failFast            If <tt>true</tt>, a {@link TooLongFrameException} is thrown as
     *                            soon as the decoder notices the length of the frame will exceed
     *                            <tt>maxFrameLength</tt> regardless of whether the entire frame
     *                            has been read.  If <tt>false</tt>, a {@link TooLongFrameException}
     *                            is thrown after the entire frame that exceeds <tt>maxFrameLength</tt>
     *                            has been read.
     */
    public ConnectorLengthFieldBasedFrameDecoder(
            ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
            int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
        if (byteOrder == null) {
            throw new NullPointerException("byteOrder");
        }

        if (maxFrameLength <= 0) {
            throw new IllegalArgumentException(
                    "maxFrameLength must be a positive integer: " +
                            maxFrameLength);
        }

        if (lengthFieldOffset < 0) {
            throw new IllegalArgumentException(
                    "lengthFieldOffset must be a non-negative integer: " +
                            lengthFieldOffset);
        }

        if (initialBytesToStrip < 0) {
            throw new IllegalArgumentException(
                    "initialBytesToStrip must be a non-negative integer: " +
                            initialBytesToStrip);
        }

        if (lengthFieldOffset > maxFrameLength - lengthFieldLength) {
            throw new IllegalArgumentException(
                    "maxFrameLength (" + maxFrameLength + ") " +
                            "must be equal to or greater than " +
                            "lengthFieldOffset (" + lengthFieldOffset + ") + " +
                            "lengthFieldLength (" + lengthFieldLength + ").");
        }

        this.byteOrder = byteOrder;
        this.maxFrameLength = maxFrameLength;
        this.lengthFieldOffset = lengthFieldOffset;
        this.lengthFieldLength = lengthFieldLength;
        this.lengthAdjustment = lengthAdjustment;
        lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength;
        this.initialBytesToStrip = initialBytesToStrip;
        this.failFast = failFast;
    }

    private void setHeader(MessageHeader header, ByteBuf decoded) {
//        header.setEncryptType(decoded.readByte());
//        byte tmp = decoded.readByte();
//        header.setProtocolVersion((byte) (tmp >> 2 & 0x000000ff));
//        header.setQos((byte) (tmp & 0x00000003));
//        header.setReservedBits(decoded.readByte());
//        int bodyLength = ((header.getReservedBits() << 16 & 0x001f0000) | (decoded.readByte()) << 8 & 0x0000ff00) | (decoded.readByte() & 0x000000ff);
//        header.setBodyLength(bodyLength);
//        header.setKeyCheck(decoded.readByte());
//        byte[] bodyCheck = new byte[bodyCheckLength];
//        decoded.readBytes(bodyCheck, 0, bodyCheck.length);
//        header.setBodyCheck(bodyCheck);
    }

    @Override
    protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        ByteBuf decoded = decode(ctx, in);
        if (decoded == null) {
            return;
        }

        try {
            byte magic = decoded.readByte();
            if (magic != MessageHeader.MAGIC) {
                log.error("Illegal packet, magic number={}", magic);
                return;
            }
            MessageHeader header = new MessageHeader();
            int business = ((decoded.readByte()) << 8 & 0x0000ff00) | (decoded.readByte() & 0x000000ff);
            header.setBusiness(business);
            RrmpMessage message = new RrmpMessage();
            if (business == BusinessType.PING) {
                setHeader(header, decoded);
                message.setHeader(header);
                out.add(message);
                return;
            }

            setHeader(header, decoded);
            byte[] bodyBytes = new byte[decoded.readableBytes()];
            decoded.readBytes(bodyBytes, 0, bodyBytes.length);

            byte[] clientBodyCheck = CipherUtil.getBodyCheckValue(bodyBytes);
            if (!Arrays.equals(clientBodyCheck, header.getBodyCheck())) {
                throw new BodyCheckException("body checksum not equal!business=" + business);
            }
            message.setHeader(header);
            message.setBody(bodyBytes);
            out.add(message);

        } catch (DecoderException e) {
            throw e;
        } catch (Exception e) {
            throw new ConnectorException("decode packet error.", e);
        } finally {
            ReferenceCountUtil.release(decoded);
        }
    }

    /**
     * Create a frame out of the {@link ByteBuf} and return it.
     *
     * @param ctx the {@link ChannelHandlerContext} which this {@link ByteToMessageDecoder} belongs to
     * @param in  the {@link ByteBuf} from which to read data
     * @return frame           the {@link ByteBuf} which represent the frame or {@code null} if no frame could
     * be created.
     */
    protected ByteBuf decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        if (discardingTooLongFrame) {
            long bytesToDiscard = this.bytesToDiscard;
            int localBytesToDiscard = (int) Math.min(bytesToDiscard, in.readableBytes());
            in.skipBytes(localBytesToDiscard);
            bytesToDiscard -= localBytesToDiscard;
            this.bytesToDiscard = bytesToDiscard;

            failIfNecessary(false);
        }
        // 消息长度最低是一个协议头的长度才可以继续读buffer 中的数据
        if (in.readableBytes() < MessageHeader.HEADER_LENGTH) {
            return null;
        }

        int businessLengthFieldOffset = in.readerIndex() + magicLength;
        int business = (int) getUnadjustedFrameLength(in, businessLengthFieldOffset, businessLength, byteOrder);
        if (business == BusinessType.PING) {
            return in.readRetainedSlice(headerLength);
        } else {
            int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
            long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieldLength, byteOrder);

            if (frameLength < 0) {
                in.skipBytes(lengthFieldEndOffset);
                throw new CorruptedFrameException(
                        "negative pre-adjustment length field: " + frameLength);
            }

            frameLength += lengthAdjustment + lengthFieldEndOffset;
            if (frameLength < lengthFieldEndOffset) {
                in.skipBytes(lengthFieldEndOffset);
                throw new CorruptedFrameException(
                        "Adjusted frame length (" + frameLength + ") is less " +
                                "than lengthFieldEndOffset: " + lengthFieldEndOffset);
            }

            if (frameLength > maxFrameLength) {
                long discard = frameLength - in.readableBytes();
                tooLongFrameLength = frameLength;

                if (discard < 0) {
                    // buffer contains more bytes then the frameLength so we can discard all now
                    in.skipBytes((int) frameLength);
                } else {
                    // Enter the discard mode and discard everything received so far.
                    discardingTooLongFrame = true;
                    bytesToDiscard = discard;
                    in.skipBytes(in.readableBytes());
                }
                failIfNecessary(true);
                return null;
            }

            // never overflows because it's less than maxFrameLength
            int frameLengthInt = (int) frameLength;
            if (in.readableBytes() < frameLengthInt) {
                return null;
            }

            if (initialBytesToStrip > frameLengthInt) {
                in.skipBytes(frameLengthInt);
                throw new CorruptedFrameException(
                        "Adjusted frame length (" + frameLength + ") is less " +
                                "than initialBytesToStrip: " + initialBytesToStrip);
            }
            in.skipBytes(initialBytesToStrip);
            return in.readRetainedSlice(frameLengthInt - initialBytesToStrip);
        }
    }

    /**
     * Decodes the specified region of the buffer into an unadjusted frame length.  The default implementation is
     * capable of decoding the specified region into an unsigned 8/16/24/32/64 bit integer.  Override this method to
     * decode the length field encoded differently.  Note that this method must not modify the state of the specified
     * buffer (e.g. {@code readerIndex}, {@code writerIndex}, and the content of the buffer.)
     *
     * @throws DecoderException if failed to decode the specified region
     */
    protected long getUnadjustedFrameLength(ByteBuf buf, int offset, int length, ByteOrder order) {
        buf = buf.order(order);
        long frameLength;
        switch (length) {
            case 1:
                frameLength = buf.getUnsignedByte(offset);
                break;
            case 2:
                frameLength = buf.getUnsignedShort(offset);
                break;
            case 3:
                frameLength = buf.getUnsignedMedium(offset);
                break;
            case 4:
                frameLength = buf.getUnsignedInt(offset);
                break;
            case 8:
                frameLength = buf.getLong(offset);
                break;
            default:
                throw new DecoderException(
                        "unsupported lengthFieldLength: " + lengthFieldLength + " (expected: 1, 2, 3, 4, or 8)");
        }
        return frameLength;
    }

    private void failIfNecessary(boolean firstDetectionOfTooLongFrame) {
        if (bytesToDiscard == 0) {
            // Reset to the initial state and tell the handlers that
            // the frame was too large.
            long tooLongFrameLength = this.tooLongFrameLength;
            this.tooLongFrameLength = 0;
            discardingTooLongFrame = false;
            if (!failFast ||
                    failFast && firstDetectionOfTooLongFrame) {
                fail(tooLongFrameLength);
            }
        } else {
            // Keep discarding and notify handlers if necessary.
            if (failFast && firstDetectionOfTooLongFrame) {
                fail(tooLongFrameLength);
            }
        }
    }

    private void fail(long frameLength) {
        if (frameLength > 0) {
            throw new TooLongFrameException(
                    "Adjusted frame length exceeds " + maxFrameLength +
                            ": " + frameLength + " - discarded");
        } else {
            throw new TooLongFrameException(
                    "Adjusted frame length exceeds " + maxFrameLength +
                            " - discarding");
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        if (cause instanceof BodyCheckException) {
            log.error("BodyCheckException: {} ChannelId: {}", cause.getMessage(), ctx.channel());
            return;
        }
        super.exceptionCaught(ctx, cause);
    }

    public class BodyCheckException extends ConnectorException {

        public BodyCheckException() {
        }

        public BodyCheckException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
            super(message, cause, enableSuppression, writableStackTrace);
        }

        public BodyCheckException(String message, Throwable cause) {
            super(message, cause);
        }

        public BodyCheckException(String message) {
            super(message);
        }

        public BodyCheckException(Throwable cause) {
            super(cause);
        }
    }

    public class ConnectorException extends RuntimeException {

        private static final long serialVersionUID = -2415788095961798823L;

        public ConnectorException() {
            super();
        }

        public ConnectorException(String message, Throwable cause,
                                  boolean enableSuppression, boolean writableStackTrace) {
            super(message, cause, enableSuppression, writableStackTrace);
        }

        public ConnectorException(String message, Throwable cause) {
            super(message, cause);
        }

        public ConnectorException(String message) {
            super(message);
        }

        public ConnectorException(Throwable cause) {
            super(cause);
        }
    }
}
